Java使用FFmpeg实现mp4转m3u8

您所在的位置:网站首页 ffmpeg rmvb转mp4格式 Java使用FFmpeg实现mp4转m3u8

Java使用FFmpeg实现mp4转m3u8

2024-01-28 04:03| 来源: 网络整理| 查看: 265

Java使用FFmpeg实现mp4转m3u8 前言FFmpegM3U8 一、需求及思路分析二、安装FFmpeg1.windows下安装FFmpeg2.linux下安装FFmpegUbuntuCentOS 三、代码实现1.引入依赖2.修改配置文件3.工具类4.Controlle调用5.Url转换MultipartFile的工具类 四、播放测试1.html2.nginx配置3.效果展示

前言

本文借鉴https://blog.csdn.net/weixin_44446784/article/details/123499468

FFmpeg

官网:https://ffmpeg.org/

FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多code都是从头开发的。

M3U8

M3U8是一种基于文本的播放列表文件格式,用于指定多个媒体文件(通常是视频或音频)的播放顺序和信息,常用于网络流媒体传输。M3U8文件通常包含一系列URL地址,用于指定媒体文件的片段(segment)或流(stream),以及相关的元数据和参数。

M3U8文件一般通过HTTP协议进行下载和访问,播放器通过解析M3U8文件获取媒体文件的地址和相关信息,并根据需要逐个下载和播放分片媒体文件,从而实现流媒体的播放。由于其开放的文本格式和广泛的支持,M3U8文件在各种流媒体应用中得到了广泛的应用,特别是在移动设备和网络直播领域。

一、需求及思路分析

使用ffmpeg,把视频文件切片成m3u8,并且通过springboot,可以实现在线的点播。 客户端上传视频到服务器,服务器对视频进行切片后,返回m3u8,封面等访问路径。可以在线的播放。

二、安装FFmpeg

下载地址:https://ffmpeg.org/download.html

1.windows下安装FFmpeg 1.点击上面的官方下载地址选择Windows进行下载 在这里插入图片描述2.下载完成后解压内容如下 在这里插入图片描述3.配置系统环境变量到解压目录的bin下边 在这里插入图片描述4.打开命令行输入ffmpeg -version查看是否安装成功 在这里插入图片描述 2.linux下安装FFmpeg Ubuntu

提示需要其他依赖,按照提示进行操作即可; 如先操作:sudo apt --fix-broken install,再继续安装:sudo apt install ffmpeg; 或者使用指令:sudo apt install ffmpeg --fix-missing

1、更新apt:sudo apt update

2、安装FFmpeg:sudo apt install ffmpeg

3、安装完成后,验证安装结果:ffmpeg -version

CentOS 1.使用命令下载 wget https://johnvansickle.com/ffmpeg/release-source/ffmpeg-4.1.tar.xz #使用命令解压: cd /root/FFmpeg tar -xvJf ffmpeg-4.1.tar.xz 2.yasm安装包 cd /root/FFmpeg wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz #下载源码包 tar zxvf yasm-1.3.0.tar.gz #解压 cd yasm-1.3.0 #进入目录 ./configure #配置 make && make install #编译安装 3.安装FFmpeg cd /root/FFmpeg/ffmpeg-4.1/ ./configure --enable-shared --prefix=/usr/local/ffmpeg-4.1 make && make install #编译安装 4.下载x264 cd /root/libx264/ yum -y install git git clone https://git.videolan.org/git/x264.git 5.安装nasm tar -xvf nasm-2.14.02.tar.gz cd nasm-2.14.02 ./configure make sudo make install #查看是否安装成功 nasm -version 6.安装FFmpeg #配置 /etc/ld.so.conf vim /etc/ld.so.conf #通过vim指令进入位于etc目录中的ld.so.conf #输入i进入插入模式,将第二行的内容插入到该文件 include ld.so.conf.d/*.conf /usr/local/ffmpeg-4.1/lib ldconfig #ldconfig 是一个动态链接库管理命令,其目的为了让动态链接库为系统所共享。 make sudo make install # ffmpeg -i /root/FFmpeg/wukel.mp4 -c:v libx264 -c:a copy -hls_key_info_file /root/FFmpeg/video_folder/20220308/test1/ -hls_time 15 -hls_playlist_type vod -hls_segment_filename %06d.ts index.m3u8 ldd ffmpeg cd /root/FFmpeg/ffmpeg-4.1 ./configure --prefix=/usr/softinstall/ffmpeg --enable-gpl --enable-shared --enable-libx264 # 配置环境变量 vim /etc/profile #配置如下 export FFMPEG_HOME=/usr/local/ffmpeg-4.1 export PATH=$FFMPEG_HOME/bin:$PATH #修改完使用命令退出 ~:wq source /etc/profile # 测试 ffmpeg -version ~~~~~~~~成功~~~~~~~~~ 三、代码实现 1.引入依赖

pom.xml

1.8 1.5.4 4.3.1-1.5.4 org.springframework.boot spring-boot-starter-web org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.bytedeco javacv ${javacv.version} org.bytedeco * org.bytedeco ffmpeg-platform ${ffmpeg.version} cn.hutool hutool-all 5.6.5 com.google.code.gson gson commons-lang commons-lang 2.6 commons-fileupload commons-fileupload 1.2.2 commons-io commons-io 2.5 commons-codec commons-codec 2.修改配置文件 server: port: 8086 app: # 存储转码视频的文件夹 video-folder: /root/FFmpeg/video_folder spring: servlet: multipart: enabled: true # 不限制文件大小 max-file-size: -1 # 不限制请求体大小 max-request-size: -1 # 临时IO目录 location: "${java.io.tmpdir}" # 不延迟解析 resolve-lazily: false # 超过1Mb,就IO到临时目录 file-size-threshold: 1MB web: resources: static-locations: - "classpath:/static/" - "file:${app.video-folder}" # 把视频文件夹目录,添加到静态资源目录列表 3.工具类

MediaInfo

import java.util.List; import com.google.gson.annotations.SerializedName; public class MediaInfo { public static class Format { @SerializedName("bit_rate") private String bitRate; public String getBitRate() { return bitRate; } public void setBitRate(String bitRate) { this.bitRate = bitRate; } } public static class Stream { @SerializedName("index") private int index; @SerializedName("codec_name") private String codecName; @SerializedName("codec_long_name") private String codecLongame; @SerializedName("profile") private String profile; } @SerializedName("streams") private List streams; @SerializedName("format") private Format format; public List getStreams() { return streams; } public void setStreams(List streams) { this.streams = streams; } public Format getFormat() { return format; } public void setFormat(Format format) { this.format = format; } }

TranscodeConfig

import lombok.Data; @Data public class TranscodeConfig { private String poster = "00:00:00.001"; // 截取封面的时间 HH:mm:ss.[SSS] private String tsSeconds = "15"; // ts分片大小,单位是秒 private String cutStart; // 视频裁剪,开始时间 HH:mm:ss.[SSS] private String cutEnd; // 视频裁剪,结束时间 HH:mm:ss.[SSS] public String getPoster() { return poster; } public void setPoster(String poster) { this.poster = poster; } public String getTsSeconds() { return tsSeconds; } public void setTsSeconds(String tsSeconds) { this.tsSeconds = tsSeconds; } public String getCutStart() { return cutStart; } public void setCutStart(String cutStart) { this.cutStart = cutStart; } public String getCutEnd() { return cutEnd; } public void setCutEnd(String cutEnd) { this.cutEnd = cutEnd; } @Override public String toString() { return "TranscodeConfig [poster=" + poster + ", tsSeconds=" + tsSeconds + ", cutStart=" + cutStart + ", cutEnd=" + cutEnd + "]"; } }

FFmpegUtils

import com.erfou.minio.demo.config.TranscodeConfig; import com.erfou.minio.demo.domain.MediaInfo; import com.google.gson.Gson; import org.apache.commons.codec.binary.Hex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import javax.crypto.KeyGenerator; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; public class FFmpegUtils { private static final Logger LOGGER = LoggerFactory.getLogger(FFmpegUtils.class); // 跨平台换行符 private static final String LINE_SEPARATOR = System.getProperty("line.separator"); /** * 生成随机16个字节的AESKEY * * @return */ private static byte[] genAesKey() { try { KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); keyGenerator.init(128); return keyGenerator.generateKey().getEncoded(); } catch (NoSuchAlgorithmException e) { return null; } } /** * 在指定的目录下生成key_info, key文件,返回key_info文件 * * @param folder * @throws IOException */ private static Path genKeyInfo(String folder) throws IOException { // AES 密钥 byte[] aesKey = genAesKey(); // AES 向量 String iv = Hex.encodeHexString(genAesKey()); // key 文件写入 Path keyFile = Paths.get(folder, "key"); Files.write(keyFile, aesKey, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); // key_info 文件写入 StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("key").append(LINE_SEPARATOR); // m3u8加载key文件网络路径 stringBuilder.append(keyFile).append(LINE_SEPARATOR); // FFmeg加载key_info文件路径 stringBuilder.append(iv); // ASE 向量 Path keyInfo = Paths.get(folder, "key_info"); Files.write(keyInfo, stringBuilder.toString().getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); return keyInfo; } /** * 指定的目录下生成 master index.m3u8 文件 * * @param file master m3u8文件地址 * @param indexPath 访问子index.m3u8的路径 * @param bandWidth 流码率 * @throws IOException */ private static void genIndex(String file, String indexPath, String bandWidth) throws IOException { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("#EXTM3U").append(LINE_SEPARATOR); stringBuilder.append("#EXT-X-STREAM-INF:BANDWIDTH=" + bandWidth).append(LINE_SEPARATOR); // 码率 stringBuilder.append(indexPath); Files.write(Paths.get(file), stringBuilder.toString().getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } /** * 转码视频为m3u8 * * @param source 源视频 * @param destFolder 目标文件夹 * @param config 配置信息 * @throws IOException * @throws InterruptedException */ public static void transcodeToM3u8(String source, String destFolder, TranscodeConfig config) throws IOException, InterruptedException { // 判断源视频是否存在 if (!Files.exists(Paths.get(source))) { throw new IllegalArgumentException("文件不存在:" + source); } // 创建工作目录 Path workDir = Paths.get(destFolder, "ts"); Files.createDirectories(workDir); // 构建命令 List commands = new ArrayList(); commands.add("ffmpeg"); commands.add("-i"); commands.add(source); // 源文件 commands.add("-c:v"); commands.add("libx264"); // 视频编码为H264 commands.add("-c:a"); commands.add("copy"); // 音频直接copy commands.add("-hls_time"); commands.add(config.getTsSeconds()); // ts切片大小 commands.add("-hls_playlist_type"); commands.add("vod"); // 点播模式 commands.add("-hls_segment_filename"); commands.add("%06d.ts"); // ts切片文件名称 if (StringUtils.hasText(config.getCutStart())) { commands.add("-ss"); commands.add(config.getCutStart()); // 开始时间 } if (StringUtils.hasText(config.getCutEnd())) { commands.add("-to"); commands.add(config.getCutEnd()); // 结束时间 } commands.add("index.m3u8"); // 生成m3u8文件 // 构建进程 Process process = new ProcessBuilder() .command(commands) .directory(workDir.toFile()) .start(); // 读取进程标准输出 new Thread(() -> { try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line = null; while ((line = bufferedReader.readLine()) != null) { LOGGER.info(line); } } catch (IOException e) { } }).start(); // 读取进程异常输出 new Thread(() -> { try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { String line = null; while ((line = bufferedReader.readLine()) != null) { LOGGER.info(line); } } catch (IOException e) { } }).start(); // 阻塞直到任务结束 if (process.waitFor() != 0) { throw new RuntimeException("视频切片异常"); } // 切出封面 if (!screenShots(source, String.join(File.separator, destFolder, "poster.jpg"), config.getPoster())) { throw new RuntimeException("封面截取异常"); } // 获取视频信息 final MediaInfo[] mediaInfo = {getMediaInfo(source)}; if (mediaInfo[0] == null) { throw new RuntimeException("获取媒体信息异常"); } // 生成index.m3u8文件 // genIndex(String.join(File.separator, destFolder, "index.m3u8"), "ts/index.m3u8", mediaInfo[0].getFormat().getBitRate()); } /** * 获取视频文件的媒体信息 * * @param source * @return * @throws IOException * @throws InterruptedException */ public static MediaInfo getMediaInfo(String source) throws IOException, InterruptedException { List commands = new ArrayList(); commands.add("ffprobe"); commands.add("-i"); commands.add(source); commands.add("-show_format"); commands.add("-show_streams"); commands.add("-print_format"); commands.add("json"); Process process = new ProcessBuilder(commands) .start(); MediaInfo mediaInfo = null; try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { mediaInfo = new Gson().fromJson(bufferedReader, MediaInfo.class); } catch (IOException e) { e.printStackTrace(); } if (process.waitFor() != 0) { return null; } return mediaInfo; } /** * 截取视频的指定时间帧,生成图片文件 * * @param source 源文件 * @param file 图片文件 * @param time 截图时间 HH:mm:ss.[SSS] * @throws IOException * @throws InterruptedException */ public static boolean screenShots(String source, String file, String time) throws IOException, InterruptedException { List commands = new ArrayList(); commands.add("ffmpeg"); commands.add("-i"); commands.add(source); commands.add("-ss"); commands.add(time); commands.add("-y"); commands.add("-q:v"); commands.add("1"); commands.add("-frames:v"); commands.add("1"); commands.add("-f"); commands.add("image2"); commands.add(file); Process process = new ProcessBuilder(commands) .start(); // 读取进程标准输出 new Thread(() -> { try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line = null; while ((line = bufferedReader.readLine()) != null) { LOGGER.info(line); } } catch (IOException e) { } }).start(); // 读取进程异常输出 new Thread(() -> { try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { String line = null; while ((line = bufferedReader.readLine()) != null) { LOGGER.error(line); } } catch (IOException e) { } }).start(); return process.waitFor() == 0; } } 4.Controlle调用 import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Map; import java.util.UUID; import com.erfou.minio.demo.config.TranscodeConfig; import com.erfou.minio.demo.utils.FFmpegUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/uploadController") @Slf4j public class UploadController { @Value("${app.video-folder}") private String videoFolder; private Path tempDir = Paths.get(System.getProperty("java.io.tmpdir")); /** * 上传视频进行切片处理,返回访问路径 * @param video * @return * @throws IOException */ @PostMapping("/upload") @CrossOrigin public Object upload (@RequestParam(name = "file") MultipartFile video) throws IOException { /** 参数传UUID去数据库查询需要转换的视频地址 进行入参 public ResponseData upload (@RequestParam("uuid") String uuid) throws Exception { TranscodeConfig transcodeConfig = new TranscodeConfig(); FastDfsFile fastDfsFile = sectionService.getSectionByUUID(uuid); if(fastDfsFile.getFastDfsFileUrl() == null){ LOGGER.info("请上传视频!!"); return ResponseData.warnWithMsg("请选择要上传的视频!"); } MultipartFile video = UrlToMultipartFile.urlToMultipartFile(fastDfsFile.getFastDfsFileUrl()); */ TranscodeConfig transcodeConfig = new TranscodeConfig(); log.info("文件信息:title={}, size={}", video.getOriginalFilename(), video.getSize()); log.info("转码配置:{}", transcodeConfig); // 原始文件名称,也就是视频的标题 String title = video.getOriginalFilename(); // io到临时文件 Path tempFile = tempDir.resolve(title); log.info("io到临时文件:{}", tempFile.toString()); try { video.transferTo(tempFile); // 删除后缀 title = title.substring(0, title.lastIndexOf(".")) + "-" + UUID.randomUUID().toString().replaceAll("-", ""); // 按照日期生成子目录 String today = DateTimeFormatter.ofPattern("yyyyMMdd").format(LocalDate.now()); // 尝试创建视频目录 Path targetFolder = Files.createDirectories(Paths.get(videoFolder, today, title)); log.info("创建文件夹目录:{}", targetFolder); Files.createDirectories(targetFolder); // 执行转码操作 log.info("开始转码"); try { FFmpegUtils.transcodeToM3u8(tempFile.toString(), targetFolder.toString(), transcodeConfig); } catch (Exception e) { log.error("转码异常:{}", e.getMessage()); Map result = new HashMap(); result.put("success", false); result.put("message", e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result); } // 封装结果 Map videoInfo = new HashMap(); videoInfo.put("title", title); videoInfo.put("m3u8", String.join("/", "", today, title, "ts/index.m3u8")); videoInfo.put("poster", String.join("/", "", today, title, "poster.jpg")); //返回数据 Map result = new HashMap(); result.put("success", true); result.put("data", videoInfo); return result; } finally { // 始终删除临时文件 Files.delete(tempFile); } } }

调用 在这里插入图片描述

5.Url转换MultipartFile的工具类

如controller中参数传的是URL 使用以下工具类转换一下即可 UrlToMultipartFile

import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileItemFactory; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.lang.RandomStringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.commons.CommonsMultipartFile; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; public class UrlToMultipartFile { private static final Logger LOGGER = LoggerFactory.getLogger(UrlToMultipartFile.class); /** * inputStream 转 File */ public static File inputStreamToFile(InputStream ins, String name) throws Exception{ //System.getProperty("java.io.tmpdir")临时目录+File.separator目录中间的间隔符+文件名 File file = new File(System.getProperty("java.io.tmpdir") + File.separator + name); OutputStream os = new FileOutputStream(file); int bytesRead; int len = 8192; byte[] buffer = new byte[len]; while ((bytesRead = ins.read(buffer, 0, len)) != -1) { os.write(buffer, 0, bytesRead); } os.close(); ins.close(); return file; } /** * file转multipartFile */ public static MultipartFile fileToMultipartFile(File file) { FileItemFactory factory = new DiskFileItemFactory(16, null); FileItem item=factory.createItem(file.getName(),"text/plain",true,file.getName()); int bytesRead = 0; byte[] buffer = new byte[8192]; try { FileInputStream fis = new FileInputStream(file); OutputStream os = item.getOutputStream(); while ((bytesRead = fis.read(buffer, 0, 8192)) != -1) { os.write(buffer, 0, bytesRead); } os.close(); fis.close(); } catch (IOException e) { e.printStackTrace(); } return new CommonsMultipartFile(item); } //url转MultipartFile public static MultipartFile urlToMultipartFile(String url) throws Exception { File file = null; MultipartFile multipartFile = null; try { HttpURLConnection httpUrl = (HttpURLConnection) new URL(url).openConnection(); httpUrl.connect(); file = UrlToMultipartFile.inputStreamToFile(httpUrl.getInputStream(),RandomStringUtils.randomAlphanumeric(8)+".mp4"); LOGGER.info("---------"+file+"-------------"); multipartFile = UrlToMultipartFile.fileToMultipartFile(file); httpUrl.disconnect(); } catch (Exception e) { e.printStackTrace(); } return multipartFile; } } 四、播放测试 1.html

为了方便测试,写了一个简单的html,html只需要解压后,修改里面的src地址,设置为实际的m3u8播放地址 在这里插入图片描述

2.nginx配置 location /hls { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Headers X-Requested-With; add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS; types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; } alias D:/m3u8/hls/; #切片存放地址 expires -1; add_header Cache-Control no-cache; } 3.效果展示

在这里插入图片描述



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3